package org.jflame.commons.csv;

import org.jflame.commons.excel.ExcelColumn;
import org.jflame.commons.excel.ExcelColumnProperty;
import org.jflame.commons.excel.ExcelConvertUtils;
import org.jflame.commons.model.Chars;
import org.jflame.commons.util.ArrayHelper;
import org.jflame.commons.util.CollectionHelper;

import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

/**
 * csv writer,基于开源项目<a href="https://sourceforge.net/projects/javacsv/">javacsv</a>改造,增加<code>@ExcelColumn</code>注解支持.
 * 示例1:
 * 
 * <pre>
 * String[] headers = { "ID","单号","用户","支付金额" };
 * try (CsvWriter csvWriter = new CsvWriter("/home/user/test.csv")) {
 *     // 写标题栏
 *     csvWriter.writeRecord(headers);
 * 
 *     // 写入内容
 *     for (int i = 0; i < 10; i++) {
 *         String id = String.valueOf(i);
 *         String orderNum = UUID.randomUUID()
 *                 .toString();
 *         String userName = "用户" + i;
 *         String payMoney = String.valueOf(Math.random());
 *         // 单行写入
 *         csvWriter.writeRecord(new String[] { id,orderNum,userName,payMoney });
 *     }
 * } catch (Exception e) {
 *     e.printStackTrace();
 * }
 * </pre>
 * 
 * 示例2:
 * 
 * <pre>
 * String[] headers = { "ID","单号","用户","支付金额" };//标题
 * List&lt;Object[]&gt; contents=....   //准备内容
 * try (CsvWriter csvWriter = new CsvWriter("/home/user/test.csv")) {
 *      csvWriter.writeArrayData(headers,contents)
 * }
 * </pre>
 * 
 * 示例3:
 * 
 * <pre>
 * 
 * class Order {
 * 
 *     &#64;ExcelColumn(name = "ID", order = 1)
 *     private Integer id;
 *     &#64;ExcelColumn(name = "单号", order = 2)
 *     private Integer id;
 *     ....
 * }
 * 
 * List&lt;Order&gt; contents=...//准备内容
 * try (CsvWriter csvWriter = new CsvWriter("/home/user/test.csv")) {
 *      csvWriter.writeEntityData(contents);
 * }
 * 
 * //便捷静态方法 CsvWriter.export("/home/user/test.csv",contents);
 * </pre>
 * 
 * @see ExcelColumn
 */
public class CsvWriter implements Closeable {

    private Writer outputStream = null;
    private String fileName = null;
    private boolean firstColumn = true;
    private boolean useCustomRecordDelimiter = false;
    private Charset charset;
    // this holds all the values for switches that the user is allowed to set
    private UserSettings userSettings = new UserSettings();

    private boolean initialized = false;
    private boolean closed = false;
    private Boolean utf8bom;
    private String systemRecordDelimiter = System.getProperty("line.separator");

    /**
     * Double up the text qualifier to represent an occurrence of the text qualifier.
     */
    public static final int ESCAPE_MODE_DOUBLED = 1;

    /**
     * Use a backslash character before the text qualifier to represent an occurrence of the text qualifier.
     */
    public static final int ESCAPE_MODE_BACKSLASH = 2;

    /**
     * 构造函数
     * 
     * @param fileName csv文件路径
     * @param delimiter 分隔符
     * @param charset 编码
     */
    public CsvWriter(String fileName, char delimiter, Charset charset) {
        if (fileName == null) {
            throw new IllegalArgumentException("Parameter fileName can not be null.");
        }

        if (charset == null) {
            throw new IllegalArgumentException("Parameter charset can not be null.");
        }

        this.fileName = fileName;
        userSettings.Delimiter = delimiter;
        this.charset = charset;
    }

    /**
     * 构造函数.默认使用,号分隔,utf-8编码
     * 
     * @param fileName csv文件路径
     */
    public CsvWriter(String fileName) {
        this(fileName, Chars.COMMA, StandardCharsets.UTF_8);
    }

    public CsvWriter(File file) throws IOException {
        this(Files.newBufferedWriter(file.toPath()), Chars.COMMA);
    }

    public CsvWriter(Path csvPath) throws IOException {
        this(Files.newBufferedWriter(csvPath), Chars.COMMA);
    }

    /**
     * Creates a {@link CsvWriter} object using a Writer to write data to.
     * 
     * @param outputStream The stream to write the column delimited data to.
     * @param delimiter The character to use as the column delimiter.
     */
    public CsvWriter(Writer outputStream, char delimiter) {
        if (outputStream == null) {
            throw new IllegalArgumentException("Parameter outputStream can not be null.");
        }

        this.outputStream = outputStream;
        userSettings.Delimiter = delimiter;
        initialized = true;
    }

    /**
     * Creates a {@link CsvWriter CsvWriter} object using an OutputStream to write data to.
     * 
     * @param outputStream The stream to write the column delimited data to.
     * @param delimiter The character to use as the column delimiter.
     * @param charset The {@link java.nio.charset.Charset Charset} to use while writing the data.
     */
    public CsvWriter(OutputStream outputStream, char delimiter, Charset charset) {
        this(new OutputStreamWriter(outputStream, charset), delimiter);
    }

    public CsvWriter(OutputStream outputStream) {
        this(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), Chars.COMMA);
    }

    /**
     * Gets the character being used as the column delimiter.
     * 
     * @return The character being used as the column delimiter.
     */
    public char getDelimiter() {
        return userSettings.Delimiter;
    }

    /**
     * Sets the character to use as the column delimiter.
     * 
     * @param delimiter The character to use as the column delimiter.
     */
    public void setDelimiter(char delimiter) {
        userSettings.Delimiter = delimiter;
    }

    public char getRecordDelimiter() {
        return userSettings.RecordDelimiter;
    }

    /**
     * Sets the character to use as the record delimiter.
     * 
     * @param recordDelimiter The character to use as the record delimiter. Default is combination of standard end of
     *            line characters for Windows, Unix, or Mac.
     */
    public void setRecordDelimiter(char recordDelimiter) {
        useCustomRecordDelimiter = true;
        userSettings.RecordDelimiter = recordDelimiter;
    }

    /**
     * Gets the character to use as a text qualifier in the data.
     * 
     * @return The character to use as a text qualifier in the data.
     */
    public char getTextQualifier() {
        return userSettings.TextQualifier;
    }

    /**
     * Sets the character to use as a text qualifier in the data.
     * 
     * @param textQualifier The character to use as a text qualifier in the data.
     */
    public void setTextQualifier(char textQualifier) {
        userSettings.TextQualifier = textQualifier;
    }

    /**
     * Whether text qualifiers will be used while writing data or not.
     * 
     * @return Whether text qualifiers will be used while writing data or not.
     */
    public boolean getUseTextQualifier() {
        return userSettings.UseTextQualifier;
    }

    /**
     * Sets whether text qualifiers will be used while writing data or not.
     * 
     * @param useTextQualifier Whether to use a text qualifier while writing data or not.
     */
    public void setUseTextQualifier(boolean useTextQualifier) {
        userSettings.UseTextQualifier = useTextQualifier;
    }

    public int getEscapeMode() {
        return userSettings.EscapeMode;
    }

    public void setEscapeMode(int escapeMode) {
        userSettings.EscapeMode = escapeMode;
    }

    public void setComment(char comment) {
        userSettings.Comment = comment;
    }

    public char getComment() {
        return userSettings.Comment;
    }

    /**
     * Whether fields will be surrounded by the text qualifier even if the qualifier is not necessarily needed to escape
     * this field.
     * 
     * @return Whether fields will be forced to be qualified or not.
     */
    public boolean getForceQualifier() {
        return userSettings.ForceQualifier;
    }

    /**
     * 所有字段强制使用限定符(如双引号)包围.默认 false.
     * 
     * @param forceQualifier Whether to force the fields to be qualified or not.
     */
    public void setForceQualifier(boolean forceQualifier) {
        userSettings.ForceQualifier = forceQualifier;
    }

    public Boolean getUtf8bom() {
        return utf8bom;
    }

    public void setUtf8bom(Boolean utf8bom) {
        this.utf8bom = utf8bom;
    }

    final char SPACE = ' ';
    final char POUND = '#';
    final String DOUBLE_BACKSLASH = "\\\\";
    final String BACKSLASH = "\\";

    /**
     * 将内容写入在当前行新列
     * 
     * @param content The data for the new column.
     * @param preserveSpaces 是否保留内容首尾空格,true=保留,false删除首尾空格
     * @exception CsvAccessException Thrown if an error occurs while writing data to the destination stream.
     */
    public void write(String content, boolean preserveSpaces) throws CsvAccessException {
        checkClosed();
        checkInit();

        if (content == null) {
            content = StringUtils.EMPTY;
        }
        boolean textQualify = userSettings.ForceQualifier;

        if (!preserveSpaces && content.length() > 0) {
            content = content.trim();
        }

        if (!textQualify && userSettings.UseTextQualifier && (content.indexOf(userSettings.TextQualifier) > -1
                || content.indexOf(userSettings.Delimiter) > -1
                || (!useCustomRecordDelimiter && (content.indexOf(Chars.LF) > -1 || content.indexOf(Chars.CR) > -1))
                || (useCustomRecordDelimiter && content.indexOf(userSettings.RecordDelimiter) > -1)
                || (firstColumn && content.length() > 0 && content.charAt(0) == userSettings.Comment) ||
                // check for empty first column, which if on its own line must
                // be qualified or the line will be skipped
                (firstColumn && content.length() == 0))) {
            textQualify = true;
        }

        if (userSettings.UseTextQualifier && !textQualify && content.length() > 0 && preserveSpaces) {
            char firstLetter = content.charAt(0);

            if (firstLetter == SPACE || firstLetter == TAB) {
                textQualify = true;
            }

            if (!textQualify && content.length() > 1) {
                char lastLetter = content.charAt(content.length() - 1);

                if (lastLetter == SPACE || lastLetter == TAB) {
                    textQualify = true;
                }
            }
        }
        try {
            if (!firstColumn) {
                outputStream.write(userSettings.Delimiter);
            }

            if (textQualify) {
                outputStream.write(userSettings.TextQualifier);

                if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH) {
                    content = StringUtils.replace(content, BACKSLASH, DOUBLE_BACKSLASH);
                    content = StringUtils.replace(content, String.valueOf(userSettings.TextQualifier),
                            BACKSLASH + userSettings.TextQualifier);
                } else {
                    content = StringUtils.replace(content, String.valueOf(userSettings.TextQualifier),
                            String.valueOf(userSettings.TextQualifier) + userSettings.TextQualifier);
                }
            } else if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH) {
                content = StringUtils.replace(content, BACKSLASH, DOUBLE_BACKSLASH);
                content = StringUtils.replace(content, String.valueOf(userSettings.Delimiter),
                        BACKSLASH + userSettings.Delimiter);
                if (useCustomRecordDelimiter) {
                    content = StringUtils.replace(content, String.valueOf(userSettings.RecordDelimiter),
                            BACKSLASH + userSettings.RecordDelimiter);
                } else {
                    content = StringUtils.replace(content, StringUtils.CR, BACKSLASH + Chars.CR);
                    content = StringUtils.replace(content, StringUtils.LF, BACKSLASH + Chars.LF);
                }

                if (firstColumn && content.length() > 0 && content.charAt(0) == userSettings.Comment) {
                    if (content.length() > 1) {
                        content = BACKSLASH + userSettings.Comment + content.substring(1);
                    } else {
                        content = BACKSLASH + userSettings.Comment;
                    }
                }
            }

            outputStream.write(content);

            if (textQualify) {
                outputStream.write(userSettings.TextQualifier);
            }
        } catch (IOException e) {
            throw new CsvAccessException(e);
        }
        firstColumn = false;
    }

    /**
     * Writes another column of data to this record.&nbsp;Does not preserve leading and trailing whitespace in this
     * column of data.
     * 
     * @param content The data for the new column.
     * @exception CsvAccessException Thrown if an error occurs while writing data to the destination stream.
     */
    public void write(String content) throws CsvAccessException {
        write(content, false);
    }

    public void writeComment(String commentText) throws CsvAccessException {
        checkClosed();
        checkInit();
        try {
            outputStream.write(userSettings.Comment);
            outputStream.write(commentText);
            if (useCustomRecordDelimiter) {
                outputStream.write(userSettings.RecordDelimiter);
            } else {
                outputStream.write(systemRecordDelimiter);
            }
        } catch (IOException e) {
            throw new CsvAccessException(e);
        }
        firstColumn = true;
    }

    /**
     * 写入单行数据
     * 
     * @param values Values to be written.
     * @param preserveSpaces 是否保留内容首尾空格,true=保留,false删除首尾空格
     * @throws CsvAccessException Thrown if an error occurs while writing data to the destination stream.
     */
    public void writeRecord(String[] values, boolean preserveSpaces) throws CsvAccessException {
        if (values != null && values.length > 0) {
            for (int i = 0; i < values.length; i++) {
                write(values[i], preserveSpaces);
            }

            endRecord();
        }
    }

    /**
     * 写入单行数据
     * 
     * @param values Values to be written.
     */
    public void writeRecord(String[] values) {
        writeRecord(values, false);
    }

    public <T> void writeEntityData(Collection<T> dataList) throws CsvAccessException {
        writeEntityData(dataList, null);
    }

    static final Pattern LONG_DIGIT_TEXT_REGX = Pattern.compile("\\d{12,}");

    @SuppressWarnings("unchecked")
    public <T> void writeEntityData(Collection<T> dataList, String group) throws CsvAccessException {
        if (CollectionHelper.isEmpty(dataList)) {
            return;
        }
        Class<T> dataClass = null;
        Iterator<T> it = dataList.iterator();
        while (it.hasNext()) {
            T t = it.next();
            if (t != null) {
                dataClass = (Class<T>) t.getClass();
                break;
            }
        }

        List<ExcelColumnProperty> columnPropertys = ExcelColumnProperty.resolveColumnProperty(dataClass, true,
                Optional.ofNullable(group), null);
        if (CollectionHelper.isEmpty(columnPropertys)) {
            throw new CsvAccessException(
                    "No field with @ExcelColumn annotation was found in the " + dataClass.getName());
        }
        String[] headers = columnPropertys.stream()
                .map(ExcelColumnProperty::getName)
                .toArray(String[]::new);
        String currentStrVal;
        Object currentValue;
        try {
            writeRecord(headers);
            for (T rowData : dataList) {
                for (ExcelColumnProperty currentProperty : columnPropertys) {
                    currentValue = currentProperty.getPropertyDescriptor()
                            .getReadMethod()
                            .invoke(rowData);
                    if (currentValue == null) {
                        write(StringUtils.EMPTY);
                    } else {
                        currentStrVal = ExcelConvertUtils.convertToCellValue(currentProperty, currentValue);
                        // 超11位的纯数字文本类型(如条码)输出用Excel打开时会显示为科学计数法
                        if ("0".equals(currentProperty.getFmt()) && LONG_DIGIT_TEXT_REGX.matcher(currentStrVal)
                                .matches()) {
                            write(currentStrVal + TAB, true);
                        } else {
                            write(currentStrVal);
                        }
                    }
                }
                endRecord();
            }
        } catch (Exception e) {
            throw new CsvAccessException("写入CSV失败", e);
        }

    }

    final char TAB = '\t';

    public void writeArrayData(String[] titles, List<Object[]> dataList) throws CsvAccessException {
        if (CollectionHelper.isEmpty(dataList)) {
            return;
        }
        if (ArrayHelper.isNotEmpty(titles)) {
            writeRecord(titles);
        }
        int index = 0;
        String tmpVal;
        List<ExcelColumnProperty> columnPropertys = ExcelColumnProperty.resolveColumnProperty(null, dataList);
        ExcelColumnProperty currentProperty;
        try {
            for (Object[] rowData : dataList) {
                for (index = 0; index < rowData.length; index++) {
                    if (rowData[index] == null) {
                        write(StringUtils.EMPTY);
                    } else {
                        currentProperty = columnPropertys.get(index);
                        tmpVal = ExcelConvertUtils.convertToCellValue(currentProperty, rowData[index]);
                        // 超11位的纯数字文本类型(如条码)输出用Excel打开时会显示为科学计数法
                        if ("0".equals(currentProperty.getFmt()) && LONG_DIGIT_TEXT_REGX.matcher(tmpVal)
                                .matches()) {
                            write(tmpVal + TAB, true);
                        } else {
                            write(tmpVal);
                        }
                    }

                }
                endRecord();
            }
        } catch (Exception e) {
            throw new CsvAccessException("写入CSV失败", e);
        }
    }

    /**
     * 结束一行写入
     * 
     * @exception CsvAccessException Thrown if an error occurs while writing data to the destination stream.
     */
    public void endRecord() throws CsvAccessException {
        checkClosed();

        checkInit();
        try {
            if (useCustomRecordDelimiter) {
                outputStream.write(userSettings.RecordDelimiter);
            } else {
                outputStream.write(systemRecordDelimiter);
            }
        } catch (IOException e) {
            throw new CsvAccessException(e);
        }

        firstColumn = true;
    }

    private void checkInit() {
        if (!initialized) {
            if (fileName != null) {
                try {
                    outputStream = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), charset));
                    if (Boolean.TRUE.equals(utf8bom) && charset.equals(StandardCharsets.UTF_8)) {
                        outputStream.write(new String(new byte[] { (byte) 0xEF,(byte) 0xBB,(byte) 0xBF }));
                    }
                } catch (IOException e) {
                    throw new CsvAccessException(e);
                }
            }

            initialized = true;
        }
    }

    /**
     * Clears all buffers for the current writer and causes any buffered data to be written to the underlying device.
     * 
     * @exception IOException Thrown if an error occurs while writing data to the destination stream.
     */
    public void flush() throws IOException {
        outputStream.flush();
    }

    /**
     * 关闭并释放资源
     */
    @Override
    public void close() {
        if (!closed) {
            close(true);
            closed = true;
        }
    }

    /**
     * 关闭并释放资源
     */
    private void close(boolean closing) {
        if (!closed) {
            if (closing) {
                charset = null;
            }

            try {
                if (initialized) {
                    outputStream.close();
                }
            } catch (Exception e) {
                // just eat the exception
            }
            outputStream = null;
            closed = true;
        }
    }

    /**
     * 判断是否已关闭
     */
    private void checkClosed() {
        if (closed) {
            throw new CsvAccessException("This instance of the CsvWriter class has already been closed.");
        }
    }

    protected void finalize() {
        close(false);
    }

    private class UserSettings {

        // having these as publicly accessible members will prevent
        // the overhead of the method call that exists on properties
        public char TextQualifier;

        public boolean UseTextQualifier;

        public char Delimiter;

        public char RecordDelimiter;

        public char Comment;

        public int EscapeMode;

        public boolean ForceQualifier;

        public UserSettings() {
            TextQualifier = '"';
            UseTextQualifier = true;
            Delimiter = Chars.COMMA;
            RecordDelimiter = Chars.NULL;
            Comment = POUND;
            EscapeMode = ESCAPE_MODE_DOUBLED;
            ForceQualifier = false;
        }
    }

    /**
     * 实体数据生成csv文件
     * 
     * @param csvFile csv文件路径
     * @param dataList 数据集合,实现IExcelEntity接口的实体数据集
     * @throws CsvAccessException 生成过程任何异常
     */
    public static <T> void export(final String csvFile, final Collection<T> dataList, final String dataGroup)
            throws CsvAccessException {
        try (CsvWriter csvWriter = new CsvWriter(csvFile)) {
            csvWriter.writeEntityData(dataList, dataGroup);
        }
    }

    public static <T> void export(final String csvFile, final Collection<T> dataList) throws CsvAccessException {
        export(csvFile, dataList, null);
    }

    /**
     * 实体数据生成csv文件
     * 
     * @param outputStream 输出流
     * @param dataList 数据集合,实现IExcelEntity接口的实体数据集
     * @throws CsvAccessException 生成过程任何异常
     */
    public static <T> void export(OutputStream outputStream, Collection<T> dataList) throws CsvAccessException {
        export(outputStream, dataList, null);
    }

    public static <T> void export(final OutputStream outputStream, final Collection<T> dataList, final String dataGroup)
            throws CsvAccessException {
        try (CsvWriter csvWriter = new CsvWriter(outputStream)) {
            csvWriter.writeEntityData(dataList, dataGroup);
        }
    }

    /* public static String replace(String original, String pattern, String replace) {
        final int len = pattern.length();
        int found = original.indexOf(pattern);
    
        if (found > -1) {
            StringBuffer sb = new StringBuffer();
            int start = 0;
    
            while (found != -1) {
                sb.append(original.substring(start, found));
                sb.append(replace);
                start = found + len;
                found = original.indexOf(pattern, start);
            }
    
            sb.append(original.substring(start));
    
            return sb.toString();
        } else {
            return original;
        }
    }*/
}